/*
 * Copyright 2022 The Open Islands Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.openislands.oi.console.config;

import org.jline.keymap.KeyMap;
import org.jline.reader.*;
import org.jline.reader.impl.completer.AggregateCompleter;
import org.jline.reader.impl.completer.ArgumentCompleter;
import org.jline.terminal.Attributes;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
import org.openislands.oi.console.config.annotation.Command;
import org.openislands.oi.console.config.annotation.CommandArgs;
import org.openislands.oi.console.config.annotation.CommandParam;
import org.openislands.oi.console.config.bean.CommandGroupContext;
import org.openislands.oi.console.config.bean.SingleCommandGroupContext;
import org.openislands.oi.console.config.completer.FirstCompleter;
import org.openislands.oi.console.utils.ParameterUtils;
import org.openislands.oi.constant.Dict;
import org.openislands.oi.util.StringUtils;

import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;

public class LineReaderConfig {
    public static final String spilt = "====================================================================";
    public static final String prompt = "[oi]> ";
    public static final String paramPrefix = "--";
    public static final String space = " ";
    public static final String SIMPLE = "simple";
    //public static final String COMMON = "common";

    // start auto-completion
    public static final boolean AUTO_COMPLETER = true;
    // important reminder collection
    protected static final Set<String> tipsSet = new HashSet<>();

    // completion hint collection
    protected static final List<Completer> completerList = new ArrayList<>();
    private static final Set<String> helpCommand = new HashSet<>(Arrays.asList("-h", "--help", "help"));
    private static final Set<String> exitCommand = new HashSet<>(Arrays.asList("exit", "q", "quit"));

    public static void addCompleter(Completer completer) {
        completerList.add(completer);
    }

    public static Candidate simpleCandidate(String groupName, String candidateName) {
        return new Candidate(candidateName, candidateName, groupName, null, null, null, true);
    }

    public static String getHistoryFile() {
        String config = System.getProperty(Dict.OI_EXTERNAL_CONFIG);
        return config != null ? config.endsWith("/") ? config + "console.history" : config + "/console.history" : null;
    }

    public static LineReader createLineReader() throws IOException {
        completerList.add(new ArgumentCompleter(
                new FirstCompleter(
                        simpleCandidate(SIMPLE, "help"),
                        simpleCandidate(SIMPLE, "-h"),
                        simpleCandidate(SIMPLE, "--help"),
                        simpleCandidate(SIMPLE, "exit"),
                        simpleCandidate(SIMPLE, "q"),
                        simpleCandidate(SIMPLE, "quit")
                )
        ));

        Terminal terminal = TerminalBuilder.builder()
                .nativeSignals(true).signalHandler(Terminal.SignalHandler.SIG_IGN).build();

        Attributes attributes = terminal.getAttributes();
        // enable CTRL+D shortcut to exit
        // disable CTRL+C shortcut
        attributes.setControlChar(Attributes.ControlChar.VEOF, 4);
        attributes.setControlChar(Attributes.ControlChar.VINTR, -1);
        terminal.setAttributes(attributes);

        LineReader lineReader = LineReaderBuilder.builder()
                .terminal(terminal)
                .completer(new AggregateCompleter(completerList))
                .variable(LineReader.HISTORY_FILE, getHistoryFile())
                .build()
                .option(LineReader.Option.HISTORY_IGNORE_SPACE, false)
                .option(LineReader.Option.HISTORY_REDUCE_BLANKS, false);

        if (AUTO_COMPLETER) {
            KeyMap<Binding> keymap = lineReader.getKeyMaps().get(LineReader.MAIN);
            keymap.bind(new Reference("beginning-of-line"), "\033[1~");
            keymap.bind(new Reference("end-of-line"), "\033[4~");
        }

        return lineReader;
    }

    public static void welcome() {
        OIConsole.console(spilt);
        OIConsole.console("Welcome use open islands console!");
        OIConsole.console("Use 'help' for help. 'quit' or 'q' or 'exit' to quit console, command '--help'/'-h' query single command detail.");
        OIConsole.console(
                "   ____                     _____     _                 _     \n" +
                        "  / __ \\                   |_   _|   | |               | |    \n" +
                        " | |  | |_ __   ___ _ __     | |  ___| | __ _ _ __   __| |___ \n" +
                        " | |  | | '_ \\ / _ \\ '_ \\    | | / __| |/ _` | '_ \\ / _` / __|\n" +
                        " | |__| | |_) |  __/ | | |  _| |_\\__ \\ | (_| | | | | (_| \\__ \\\n" +
                        "  \\____/| .__/ \\___|_| |_| |_____|___/_|\\__,_|_| |_|\\__,_|___/\n" +
                        "        | |                                                   \n" +
                        "        |_|                                                   "
        );
        OIConsole.console(spilt);
        OIConsole.console("-------");
    }

    public static void end() {
        OIConsole.console("bye");
    }

    public static void tips() {
        OIConsole.console(" help query all command, command --help/-h query single command detail. parameter use ' ' spilt, space value use \\\\' ' escaped");
    }

    public static void help() {
        OIConsole.console("command parameter please uses space spilt, space value use \\\\ escaped. use ./ ../ / complete file");
        OIConsole.console("--------- command set ---------");

        // multi
        for (Map.Entry<String, Map<String, Method>> mapEntry : CommandGroupContext.entrySet()) {
            String commandGroup = mapEntry.getKey();
            Map<String, Method> methodMap = mapEntry.getValue();
            help(commandGroup, methodMap.entrySet());
        }
        // single
        help(StringUtils.EMPTY, SingleCommandGroupContext.entrySet());

        OIConsole.console(" help(help, -h, --help)                                                                   get help");
        OIConsole.console(" exit(exit, q, quit)                                                                      exit console");

        for (String tips : tipsSet) {
            OIConsole.console(tips);
        }
        OIConsole.console("-------");
    }

    private static void help(String commandGroup, Set<Map.Entry<String, Method>> entrySet) {
        if (!StringUtils.EMPTY.equals(commandGroup)) {
            commandGroup = commandGroup + space;
        }

        for (Map.Entry<String, Method> entry : entrySet) {
            String command = entry.getKey();
            Method method = entry.getValue();
            StringBuilder sb = new StringBuilder();
            sb.append(space).append(commandGroup).append(command);
            for (int i = 90 - sb.length(); i > 0; i--) {
                sb.append(space);
            }
            sb.append(method.getAnnotation(Command.class).desc());
            OIConsole.console(sb.toString());
        }
    }

    /***
     * Console print command group help
     * @param groupName         group
     * @param childCommandMap   group child command
     */
    public static void printHelp(String groupName, Map<String, Method> childCommandMap) {
        int indent = 1;
        OIConsole.console(groupName + "   ([*] param is required)", indent);
        for (Map.Entry<String, Method> methodEntry : childCommandMap.entrySet()) {
            String commandName = methodEntry.getKey();
            Method method = methodEntry.getValue();
            printHelp(groupName, commandName, method);
            OIConsole.console("---------", indent);
        }
    }

    /***
     * Console print command help
     * @param groupName     group
     * @param childCommand  command
     * @param method        command handle method
     */
    public static void printHelp(String groupName, String childCommand, Method method) {
        if (!StringUtils.EMPTY.equals(groupName)) {
            groupName = groupName + space;
        }

        int indent = 1;
        Command command = method.getAnnotation(Command.class);
        String desc = StringUtils.noTrimEmpty(command.desc()) ? " (" + command.desc() + ")" : "";
        OIConsole.console(childCommand + desc, indent + 2);

        StringBuilder params = new StringBuilder();
        Parameter[] parameters = method.getParameters();
        for (Parameter parameter : parameters) {
            if (parameter.isAnnotationPresent(CommandArgs.class)) {
                OIConsole.console("args ...", indent + 4);
                params.append(space).append("...");
            } else {
                CommandParam commandParam = parameter.getAnnotation(CommandParam.class);
                StringBuilder stringBuilder = new StringBuilder();
                String paramName;
                String defaultValue = "null";
                String required = "*";
                String paramDesc = "";
                if (Objects.isNull(commandParam)) {
                    paramName = defaultParamName(parameter.getName());
                } else {
                    paramName = defaultParamName(parameter.getName(), commandParam);
                    defaultValue = StringUtils.noTrimEmpty(commandParam.defaultValue())
                            ? commandParam.defaultValue() : defaultValue;
                    required = commandParam.required() && "null".equals(defaultValue) ? required : "";
                    paramDesc = commandParam.desc();
                }

                params.append(space).append(paramName).append(space).append(defaultValue);
                stringBuilder.append(paramName)
                        .append(" [").append(required).append(parameter.getName()).append("]")
                        .append(" (").append("default: ").append(defaultValue)
                        .append(", type: ").append(parameter.getType().getSimpleName());
                if (!paramDesc.isEmpty()) {
                    stringBuilder.append(", desc: ").append(paramDesc);
                }
                stringBuilder.append(")");
                OIConsole.console(stringBuilder.toString(), indent + 4);
            }
        }

        String example = "example: " + groupName + childCommand + params;
        OIConsole.console(example, indent + 2);
    }

    public static boolean containsHelpCommand(String command) {
        return helpCommand.contains(command);
    }

    public static boolean containsExitCommand(String command) {
        return exitCommand.contains(command);
    }

    public static String defaultParamName(String parameterName) {
        return paramPrefix + ParameterUtils.smallHumpTurnEnDash(parameterName);
    }

    public static String defaultParamName(String parameterName, CommandParam commandParam) {
        String paramName = StringUtils.noTrimEmpty(commandParam.name()) ? commandParam.name() : commandParam.value();
        return StringUtils.noTrimEmpty(paramName) ? paramName : defaultParamName(parameterName);
    }
}
